EKS Pod Identity を活用して Terraform でプロビジョニングした EKS を Blue/Green アップグレードしてみた
EKS Pod Identity とは?
EKS クラスター内の Pod に AWS 権限を与える新しい方式として EKS Pod Identity が発表されました。
Pod に Service Account 単位で細かく権限設定を行う際、 IRSA(IAM Roles for Service Accounts) を以前から利用できました。
こちらを使わずに EKS ノード単位で権限を付与すると Pod に必要以上の権限を付与してしまう可能性が高いため、IRSA はかなり一般的に利用されている機能かと思います。
Pod Identity も特定 Service Account 内の Pod から IAM ロールを利用可能にするための同じ目的の機能になります。
ただ、今回 Pod への権限付与を EKS 側の設定として扱うことができるようになった点が大きな違いになります。
というのも IRSA は仕組み上、利用する IAM ロールの信頼ポリシーで Open ID Connect プロバイダーから AssumeRole する許可設定を行う必要があります。
この際、下記例のように Condition.StringEquals
属性でクラスター固有の Open ID Connect プロバイダー URL、 名前空間、 Service Account 名の情報が必要になります。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::xxxxxxxxxxxx:oidc-provider/oidc.eks.ap-northeast-1.amazonaws.com/id/XXXXXXXXXXXXXXXXXXXXXXXX" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.eks.ap-northeast-1.amazonaws.com/id/XXXXXXXXXXXXXXXXXXXXXXXX:aud": "sts.amazonaws.com", "oidc.eks.ap-northeast-1.amazonaws.com/id/XXXXXXXXXXXXXXXXXXXXXXXX:sub": "system:serviceaccount:kube-system:aws-load-balancer-controller" } } } ] }
Pod Identity であれば、 IAM ロールで pods.eks.amazonaws.com
を許可しておいて、各 Service Account への実際の権限付与は EKS 側で管理することができるようになりました。
信頼ポリシーにクラスター固有の情報が無くなり、かなりシンプルになっています。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "pods.eks.amazonaws.com" }, "Action": [ "sts:TagSession", "sts:AssumeRole" ] } ] }
その上、 Pod Identity であれば IAM ロールを複数クラスターで共用する場合も信頼ポリシーを変える必要がありません。
また、IRSA を利用する場合は Service Account 側で annotations として IAM ロールの情報を指定する必要がありました。
apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/component: controller app.kubernetes.io/name: aws-load-balancer-controller name: aws-load-balancer-controller namespace: kube-system annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789123:role/alb-ingress-controller-role
EKS のマニフェストファイルと IAM ロールの信頼ポリシーを行き来して設定するよりは、EKS の設定項目としてまとめて扱った方がシンプルで設定しやすいかと思います。
何故今回 Blue/Green アップグレードを取り上げるか
今回 EKS の Blue/Green 形式のバージョンアップグレードを題材に扱うのは Pod Identity のメリットがより強く生きる場面だからです。
この場合のより具体的なメリットとして下記が 2 点が挙げられます。
- 複数クラスターで IAM ロールを共用したい場合に信頼ポリシーをシンプルにできる
- IAM ロールの信頼ポリシーに EKS クラスターの Open ID Connect プロバイダー URL を埋め込む必要が無くなるため、Terraform の State を分けた際の依存関係をシンプルにしやすい
1 つ目について、複数クラスターで IAM ロールを共用することによって信頼ポリシーが複雑になることを防ぐことができます。
もちろん IAM ロールは各クラスター固有のものを用意する整理にしても良いと思います。
iam_iam-role-for-service-accounts-eks といった IRSA で利用するための IAM ロールを作成するために便利なモジュールもあるので、各クラスター専用の IAM ロールを定義しても問題は無いかと思います。
ただ、共用できるのであれば共用したいのではないでしょうか。
EKS のバージョンアップグレードについて考えるのであれば、クラスター切り替えによって EKS 以外の設定ができる限り変わらない方が望ましいと思います。
今までは IAM ロールを共用しても新規クラスターの Open ID Connect プロバイダーを信頼ポリシーに追加して、旧クラスターの Open ID Connect プロバイダーを信頼ポリシーから削除するといった操作が必要だったので、共用しても結局 IAM ロールの設定変更を行う必要がありました。
ただ、IAM ロールの信頼ポリシーを全く触らなくて良いのであれば、よりシンプルに考えることができるようになります。(IAM ロールの信頼ポリシーはクラスター切り替えによって変化が無いことが保証される)
こちらは大きなメリットかと思います。
2 つ目は Terraform で管理している場合の話なので、必ずしも全てのケースで当てはまらないかもしれません。
下記のようなケースを考えます。
旧クラスターの横に新クラスターを立てて検証してから DNS ベースで切り替える場面を想定しています。
- VPC や Route53 Hosted Zone、 DynamoDB, IAM Role を共用します。
- Ingress リソースを用いてアプリを公開する際に、ALB のデプロイは AWS Load Balancer Controller に、レコード追加は external-dns に任せる形とします。
- アプリケーションコンテナから DynamoDB にアクセスする権限を付与する必要があります。
この際、 新規クラスターの操作によって 旧クラスターへ影響がでることを防ぐため、下記のように 3 つの State に分離して扱うこととします。
- common-environment
- 共通リソース(IAM, VPC, Route53, DynamoDB)
- eks-blue
- 旧クラスター
- eks-green
- 新規クラスター
この際、IRSA だと 新規クラスターを作成してから DynamoDB アクセス用 IAM ロールの信頼ポリシーに Open ID Connect プロバイダーの情報を埋め込む必要があります。
したがって、下記のような循環する依存関係が生まれてしまいます。
Terraform で循環参照する形になると、意図しない動作に繋がるので避けたいです。
Pod Identity を使えば、あくまで許可するのは pods.eks.amazonaws.com
なので、下記のようなシンプルな形になります。
細かい話でしたが、こういった観点でも Pod への権限付与 EKS の設定として扱えると IAM ロールを複数クラスターで共用しやすくなります。
やってみる
では実際にやってみます。
既出の図になりますが、下記構成で Blue/Green アップグレードしてみます。
構成は terraform-aws-eks-blueprints を参考にしています。
こちらは ArgoCD を採用していますが、Pod Identity とはあまり関係無いので今回は利用していません。
また、Pod から AWS 権限を扱えるかを確認したいので、DynamoDB からデータを取得して返却する fast-api のアプリに変更しています。※ コードと Dockerfile は下記
from fastapi import FastAPI import boto3 app = FastAPI() @app.get("/") def read_root(): dynamodb = boto3.client("dynamodb", region_name="ap-northeast-1") response = dynamodb.scan( TableName="test-dynamodb", ) return response["Items"]
FROM python:3.11-slim WORKDIR /app RUN pip install --upgrade pip RUN pip install boto3 fastapi uvicorn COPY ./app/ . CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]
まず、 common-environment(共用リソース) から作っていきます。
具体的な作成物は VPC, Route53 Hosted Zone, ACM, DynamoDB 読み取り用の IAM ロールになります。
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 5.2.0" name = "pod-identity-blue-green-upgrade-vpc" cidr = "10.0.0.0/16" azs = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"] public_subnets = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"] private_subnets = ["10.0.100.0/24", "10.0.101.0/24", "10.0.102.0/24"] enable_nat_gateway = true single_nat_gateway = true public_subnet_tags = { "kubernetes.io/role/elb" = 1 } private_subnet_tags = { "kubernetes.io/role/internal-elb" = 1 } } data "aws_route53_zone" "root" { name = local.hosted_zone_name } resource "aws_route53_zone" "sub" { name = "${local.sub_domain_name}.${local.hosted_zone_name}" } resource "aws_route53_record" "ns" { zone_id = data.aws_route53_zone.root.zone_id name = "${local.sub_domain_name}.${local.hosted_zone_name}" type = "NS" ttl = "30" records = aws_route53_zone.sub.name_servers } module "acm" { source = "terraform-aws-modules/acm/aws" version = "~> 5.0.0" domain_name = "${local.sub_domain_name}.${local.hosted_zone_name}" zone_id = aws_route53_zone.sub.zone_id subject_alternative_names = [ "*.${local.sub_domain_name}.${local.hosted_zone_name}" ] validation_method = "DNS" wait_for_validation = true tags = { Name = "${local.sub_domain_name}.${local.hosted_zone_name}" } } resource "aws_dynamodb_table" "test-dynamodb-table" { name = "test-dynamodb" billing_mode = "PAY_PER_REQUEST" hash_key = "UserId" attribute { name = "UserId" type = "S" } } data "aws_iam_policy_document" "allow_pod_identity" { statement { effect = "Allow" principals { type = "Service" identifiers = ["pods.eks.amazonaws.com"] } actions = [ "sts:AssumeRole", "sts:TagSession" ] } } resource "aws_iam_role" "read_dynamodb" { name = "read-dynamodb-role" assume_role_policy = data.aws_iam_policy_document.allow_pod_identity.json managed_policy_arns = ["arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess"] }
outputs.tf も定義しておきます。
output "vpc_id" { description = "The ID of the VPC" value = module.vpc.vpc_id } output "private_subnet_ids" { description = "IDs of the Private Subnets" value = module.vpc.private_subnets } output "aws_route53_zone" { description = "The new Route53 Zone" value = aws_route53_zone.sub.name } output "pod_dynamodb_role_arn" { description = "IAM Role Arn of Pod to read dynamodb" value = aws_iam_role.read_dynamodb.arn }
次に EKS クラスターを作成します。
module "eks" { source = "terraform-aws-modules/eks/aws" version = "~> 19.20.0" cluster_name = local.cluster_name cluster_version = "1.27" cluster_endpoint_public_access = true cluster_addons = { eks-pod-identity-agent = { most_recent = true } aws-efs-csi-driver = { most_recent = true } } vpc_id = data.terraform_remote_state.common-environment.outputs.vpc_id subnet_ids = data.terraform_remote_state.common-environment.outputs.private_subnet_ids eks_managed_node_groups = { initial = { node_group_name = local.node_group_name instance_types = ["m3.medium"] min_size = 2 max_size = 2 desired_size = 2 subnet_ids = data.terraform_remote_state.common-environment.outputs.private_subnet_ids } } } module "alb_ingress_controller_role" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" version = "~> 5.32.0" role_name = "alb-ingress-controller-role-blue" role_policy_arns = { policy = aws_iam_policy.alb_ingress_controller.arn } oidc_providers = { main = { provider_arn = module.eks.oidc_provider_arn namespace_service_accounts = ["kube-system:aws-load-balancer-controller"] } } } resource "aws_iam_policy" "alb_ingress_controller" { name = "alb-ingress-controller-policy-blue" policy = file("${path.module}/policy/alb-ingress-controller-policy.json") } resource "aws_iam_policy" "external_dns" { name = "external-dns-policy-blue" policy = file("${path.module}/policy/external-dns-policy.json") } module "external_dns_role" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" version = "~> 5.32.0" role_name = "external-dns-role-blue" role_policy_arns = { policy = aws_iam_policy.external_dns.arn } oidc_providers = { main = { provider_arn = module.eks.oidc_provider_arn namespace_service_accounts = ["kube-system:external-dns"] } } } resource "aws_eks_pod_identity_association" "dynamo_read" { cluster_name = module.eks.cluster_name namespace = "app" service_account = "app-sa" role_arn = data.terraform_remote_state.common-environment.outputs.pod_dynamodb_role_arn }
一番下の aws_eks_pod_identity_association リソースが Pod Identity の設定になります。
Terraform AWS Provider は v5.29.0 で既に対応済みです。
実際にアプリケーションと ingress リソースをデプロイしてアクセスすると、無事 DynamoDB の情報を Pod 経由で取得できました。
$ curl https://www.eks-test.masutaro99.com [{"UserId":{"S":"2"},"Point":{"N":"80"}},{"UserId":{"S":"1"},"Point":{"N":"70"}}]⏎
新しいクラスターを作成して、トラフィックを新規クラスターに流しても同様に DynamoDB の情報が取得できました。
Blue/Green アップグレードの際に IAM ロールを共用する構成がかなりやりやすくなりましたね。
ここで external-dns や ALB Ingress Controller についても IAM ロールを新旧クラスターで共用しても良いと思われる方もいらっしゃるかもしれません。
今回 Pod Identity を利用しなかったのは 利用可能な SDK のバージョンに制限があるためです。
例えば、SDK for Go v1 だと 1.47.11 以上でないとサポートされません。
Using a supported AWS SDK
v1.44.294 を利用している aws-load-balancer-controller は 現時点で Pod Identity を利用できません。
aws-load-balancer-controller/go.mod
同様に external-dns も検証時点では v1.44.311 を利用していたので、利用できませんでした。(現時点では v1.48.9 になっていたので使えそうでした。)
external-dns/go.mod
また、EKS アドオンも IRSA を利用する必要があるようです。
The EKS add-ons can only use IAM roles for service accounts instead.
EKS Pod Identity restrictions
Fargate 対応について
SDK のバージョンもそうですが、Fargate サポートしていない点も注意が必要です。
Linux and Windows pods that run on AWS Fargate (Fargate) aren't supported. Pods that run on Windows Amazon EC2 instances aren't supported.
EKS Pod Identity restrictions
Fargate は Service Account 単位での権限付与前提なので、Fargate こそ対応して欲しい所ですね。
Kubernetes リソースを覗いてみた
アドオンを追加すると、Daemonset として下記エージェントがインストールされます。
$ kubectl describe daemonset eks-pod-identity-agent -n kube-system Name: eks-pod-identity-agent Selector: app.kubernetes.io/instance=eks-pod-identity-agent,app.kubernetes.io/name=eks-pod-identity-agent Node-Selector: <none> Labels: app.kubernetes.io/instance=eks-pod-identity-agent app.kubernetes.io/managed-by=Helm app.kubernetes.io/name=eks-pod-identity-agent app.kubernetes.io/version=0.0.25 helm.sh/chart=eks-pod-identity-agent-1.0.0 Annotations: deprecated.daemonset.template.generation: 1 Desired Number of Nodes Scheduled: 2 Current Number of Nodes Scheduled: 2 Number of Nodes Scheduled with Up-to-date Pods: 2 Number of Nodes Scheduled with Available Pods: 2 Number of Nodes Misscheduled: 0 Pods Status: 2 Running / 0 Waiting / 0 Succeeded / 0 Failed Pod Template: Labels: app.kubernetes.io/instance=eks-pod-identity-agent app.kubernetes.io/name=eks-pod-identity-agent Init Containers: eks-pod-identity-agent-init: Image: 602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/eks/eks-pod-identity-agent:0.0.25 Port: <none> Host Port: <none> Command: /go-runner /eks-pod-identity-agent initialize Environment: <none> Mounts: <none> Containers: eks-pod-identity-agent: Image: 602401143452.dkr.ecr.ap-northeast-1.amazonaws.com/eks/eks-pod-identity-agent:0.0.25 Ports: 80/TCP, 2703/TCP Host Ports: 0/TCP, 0/TCP Command: /go-runner /eks-pod-identity-agent server Args: --port 80 --cluster-name pod-identity-cluster-green --probe-port 2703 Liveness: http-get http://localhost:probes-port/healthz delay=30s timeout=10s period=10s #success=1 #failure=3 Readiness: http-get http://localhost:probes-port/readyz delay=1s timeout=10s period=10s #success=1 #failure=30 Environment: AWS_REGION: ap-northeast-1 Mounts: <none> Volumes: <none> Priority Class Name: system-node-critical Events: <none>
実際に Pod Identity を設定した Service Account に紐づく Pod を確認すると専用の環境変数が登録されてます。
$ kubectl describe pod fast-api-app-75ff4874cb-bq8x8 -n app Name: fast-api-app-75ff4874cb-bq8x8 Namespace: app Priority: 0 Service Account: app-sa Node: ip-10-0-100-12.ap-northeast-1.compute.internal/10.0.100.12 Start Time: Wed, 06 Dec 2023 08:40:56 +0000 Labels: app.kubernetes.io/name=fast-api-app pod-template-hash=75ff4874cb Annotations: <none> Status: Running IP: 10.0.100.120 IPs: IP: 10.0.100.120 Controlled By: ReplicaSet/fast-api-app-75ff4874cb Containers: fast-api-app: Container ID: containerd://baa9ea99a38a13415b397c3827219fd48407e6ccba9ce8d479913b39295e604d Image: 123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/fast-api-app:latest Image ID: 123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/fast-api-app@sha256:3027af3d9aed3abaee6aa3220c373428a11a9db81b4729231adc2c484654b106 Port: 8000/TCP Host Port: 0/TCP State: Running Started: Wed, 06 Dec 2023 08:41:02 +0000 Ready: True Restart Count: 0 Environment: AWS_STS_REGIONAL_ENDPOINTS: regional AWS_DEFAULT_REGION: ap-northeast-1 AWS_REGION: ap-northeast-1 AWS_CONTAINER_CREDENTIALS_FULL_URI: http://169.254.170.23/v1/credentials AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE: /var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token Mounts: /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-2fnjd (ro) /var/run/secrets/pods.eks.amazonaws.com/serviceaccount from eks-pod-identity-token (ro) Conditions: Type Status Initialized True Ready True ContainersReady True PodScheduled True Volumes: eks-pod-identity-token: Type: Projected (a volume that contains injected data from multiple sources) TokenExpirationSeconds: 86400 kube-api-access-2fnjd: Type: Projected (a volume that contains injected data from multiple sources) TokenExpirationSeconds: 3607 ConfigMapName: kube-root-ca.crt ConfigMapOptional: <nil> DownwardAPI: true QoS Class: BestEffort Node-Selectors: <none> Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s node.kubernetes.io/unreachable:NoExecute op=Exists for 300s Events: <none>
最後に
複数クラスターで IAM ロールを共用する際に特に便利な機能かと思いました!
しかし、SDK のバージョンによっては使えない点や Fargate 未対応な点もありすぐ採用できるかというと怪しい面もあるかもしれません。
ただ利用できるなら大分便利に感じるので是非検証から始めてみてはいかがでしょうか?